LINQ: Fixes memory leak from Expression.Compile() in all call sites#5588
Conversation
Uses preferInterpretation: true on .NET 6+ to avoid generating JIT-compiled DynamicMethods that persist in native memory and cause unbounded growth. Benchmark results show 25x performance improvement (101ms → 4ms for 1000 iterations) which validates that IL emission is being skipped. Changes: - SubtreeEvaluator.cs: Use Compile(preferInterpretation: true) on .NET 6+ - SubtreeEvaluatorMemoryBenchmark.cs: Add benchmark tests to validate fix Fixes #5487
1457e49 to
6c3f73a
Compare
Address review comment: All benchmark tests should be in Benchmark project. - Deleted: Microsoft.Azure.Cosmos.Tests/Linq/SubtreeEvaluatorMemoryBenchmark.cs - Added: Microsoft.Azure.Cosmos.Performance.Tests/Linq/SubtreeEvaluatorBenchmark.cs Converted from MSTest to BenchmarkDotNet format.
Rewrites SubtreeEvaluatorBenchmark to call actual SubtreeEvaluator.Evaluate() for the fixed code path, while the baseline duplicates the old Compile() behavior. This addresses the review comment to not duplicate fix code in benchmarks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NaluTripician
left a comment
There was a problem hiding this comment.
LGTM. I also had copilot run an analysis on this PR and it said that this same pattern appears in 3 other places (Utilities.cs, DocumentQueryEvaluator.cs, and GeometrySqlExpressionFactory.cs). Should we also investigate those instances and see if the same leak pattern applies there?
kirankumarkolli
left a comment
There was a problem hiding this comment.
Great catch @NaluTripician! I investigated all Expression.Compile() call sites in the SDK:
| File | Line(s) | Leak Risk | Priority |
|---|---|---|---|
Linq/Utilities.cs (ExpressionSimplifier) |
104-106 | ✅ Same leak — called by ConstantFolding per query |
High |
Linq/DocumentQueryEvaluator.cs |
112, 122 | ✅ Same leak — raw SQL transform path | Moderate |
Linq/GeometrySqlExpressionFactory.cs |
45-47 | ✅ Same leak — spatial queries | Moderate |
HttpClient/HttpConnectionTracker.cs |
88 | ❌ One-time setup | N/A |
Created follow-up issue #5702 to track applying the same Compile(preferInterpretation: true) fix to all 3 remaining sites. Utilities.cs is the highest priority since ConstantFolding is in the hot path for every LINQ query.
…le() sites Extract shared ExpressionCompileHelper with runtime reflection to use Compile(preferInterpretation: true) on .NET 6+, avoiding DynamicMethod IL emission that causes native memory growth in long-running services. Applied to: - SubtreeEvaluator.cs (was already fixed in #5588, now uses shared helper) - Utilities.cs (ExpressionSimplifier - called by ConstantFolding per query) - DocumentQueryEvaluator.cs (raw SQL transform path) - GeometrySqlExpressionFactory.cs (spatial query evaluation) Fixes #5702 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AI Code Review SummaryPR: LINQ: Fixes memory leak from Expression.Compile() in SubtreeEvaluator Assessment: Good fix for a real production memory leak. The runtime reflection approach is the correct design choice given the 5 recommendations posted as inline comments — focused on eliminating per-call reflection overhead, improving benchmark coverage, adding a changelog entry, and making the pattern reusable for the 3 other |
|
🟡 Recommendation · Process: Missing CHANGELOG Entry Add a changelog entry for this customer-facing bug fix This PR fixes a production memory leak reported by users in issue #5487 (unbounded native memory growth in long-running services). Users experiencing this issue need to know which SDK version contains the fix so they can upgrade. The - [5588](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/5588) LINQ: Fixes memory leak from Expression.Compile() in SubtreeEvaluator |
…le() sites Extract shared ExpressionCompileHelper with runtime reflection to use Compile(preferInterpretation: true) on .NET 6+, avoiding DynamicMethod IL emission that causes native memory growth in long-running services. Applied to: - SubtreeEvaluator.cs (was already fixed in #5588, now uses shared helper) - Utilities.cs (ExpressionSimplifier - called by ConstantFolding per query) - DocumentQueryEvaluator.cs (raw SQL transform path) - GeometrySqlExpressionFactory.cs (spatial query evaluation) Fixes #5702 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merges the remaining Expression.Compile() call site fixes from PR #5703 (issue #5702) into the original fix PR #5588 (issue #5487). SubtreeEvaluator now uses the shared ExpressionCompileHelper instead of its own private implementation, reducing duplication. Fixes #5487 Fixes #5702 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
This PR was authored by GitHub Copilot as part of an automated issue triage workflow.
Fixes #5487
Fixes #5702
This PR fixes a memory leak in the LINQ provider where
Expression.Compile()generates JIT-compiled DynamicMethods that persist in native memory, causing growth in long-running services.Root Cause
Location:
Microsoft.Azure.Cosmos/src/Linq/SubtreeEvaluator.cs:119-120Analysis:
Expression.Lambda().Compile()emits a new DynamicMethod with generated IL on every call. DynamicMethod IL is stored in native memory (not GC-tracked), causing memory growth in long-running services with repeated LINQ query evaluation.Evidence:
Changes Made
Call sites updated (4 files)
EvaluateConstant(): original leak site (LINQ provider, the SDK continuously generates new JIT-compiled DynamicMethods (resulting in unmanaged memory growth) #5487)ExpressionSimplifier<T>.Eval(): uses generic overload, no cast neededHandleAsSqlTransformExpression(): 2 Compile() callsConstruct(): uses generic overload, no cast neededSubtreeEvaluatorBenchmark.cs (new)
[MemoryDiagnoser]lambda.Compile()code pathSubtreeEvaluator.Evaluate()to measure the real fixCompile()demonstrates unbounded memory growthBenchmark Results
Environment: Windows, .NET 8.0, Release build, ARM64
Performance (per-call)
Why this validates the fix:
Compile()must generate IL and JIT-compile (~44μs overhead)Compile(preferInterpretation: true)interprets directly (~3μs overhead)Testing
Local Validation
Breaking Changes
None
External References
Checklist
Generated by GitHub Copilot CLI Agent